import uuid
import time
import tempfile
import ifcopenshell
import math
# import argparse
import os, sys

O = 0., 0., 0.
X = 1., 0., 0.
Y = 0., 1., 0.
Z = 0., 0., 1.


# Helper function definitions

# Creates an IfcAxis2Placement3D from Location, Axis and RefDirection specified as Python tuples


def create_ifcaxis2placement(ifcfile, point=O, dir1=Z, dir2=X):
    point = ifcfile.createIfcCartesianPoint(point)
    dir1 = ifcfile.createIfcDirection(dir1)
    dir2 = ifcfile.createIfcDirection(dir2)
    axis2placement = ifcfile.createIfcAxis2Placement3D(point, dir1, dir2)
    return axis2placement


# Creates an IfcLocalPlacement from Location, Axis and RefDirection, specified as Python tuples, and relative placement


def create_ifclocalplacement(ifcfile, point=O, dir1=Z, dir2=X, relative_to=None):
    axis2placement = create_ifcaxis2placement(ifcfile, point, dir1, dir2)
    ifclocalplacement2 = ifcfile.createIfcLocalPlacement(
        relative_to, axis2placement)
    return ifclocalplacement2


# Creates an IfcPolyLine from a list of points, specified as Python tuples


def create_ifcpolyline(ifcfile, point_list):
    ifcpts = []
    for point in point_list:
        point = ifcfile.createIfcCartesianPoint(point)
        ifcpts.append(point)
    polyline = ifcfile.createIfcPolyLine(ifcpts)
    return polyline


# Creates an IfcExtrudedAreaSolid from a list of points, specified as Python tuples


def create_ifcextrudedareasolid(ifcfile, point_list, ifcaxis2placement, extrude_dir, extrusion):
    polyline = create_ifcpolyline(ifcfile, point_list)
    ifcclosedprofile = ifcfile.createIfcArbitraryClosedProfileDef(
        "AREA", None, polyline)
    ifcdir = ifcfile.createIfcDirection(extrude_dir)
    ifcextrudedareasolid = ifcfile.createIfcExtrudedAreaSolid(
        ifcclosedprofile, ifcaxis2placement, ifcdir, extrusion)
    return ifcextrudedareasolid


def create_guid(): return ifcopenshell.guid.compress(uuid.uuid1().hex)


# IFC template creation


def create_template(filename):
    timestamp = int(time.time())
    timestring = time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime(timestamp))
    creator = "UNRE BIM "
    organization = "UNRE Tech Ltd."
    application, application_version = "IfcOpenShell", ifcopenshell.version
    project_globalid, project_name = create_guid(), "Scann To Revit"

    # A template IFC file to quickly populate entity instances for an IfcProject with its dependencies
    template = """ISO-10303-21;
    HEADER;
    FILE_DESCRIPTION(('ViewDefinition [CoordinationView]'),'2;1');
    FILE_NAME('%(filename)s','%(timestring)s',('%(creator)s'),('%(organization)s'),'%(application)s','%(application)s','');
    FILE_SCHEMA(('IFC2X3'));
    ENDSEC;
    DATA;
    # 1=IFCPERSON($,$,'%(creator)s',$,$,$,$,$);
    # 2=IFCORGANIZATION($,'%(organization)s',$,$,$);
    # 3=IFCPERSONANDORGANIZATION(#1,#2,$);
    # 4=IFCAPPLICATION(#2,'%(application_version)s','%(application)s','');
    # 5=IFCOWNERHISTORY(#3,#4,$,.ADDED.,$,#3,#4,%(timestamp)s);
    # 6=IFCDIRECTION((1.,0.,0.));
    # 7=IFCDIRECTION((0.,0.,1.));
    # 8=IFCCARTESIANPOINT((0.,0.,0.));
    # 9=IFCAXIS2PLACEMENT3D(#8,#7,#6);
    # 10=IFCDIRECTION((0.,1.,0.));
    # 11=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.E-05,#9,#10);
    # 12=IFCDIMENSIONALEXPONENTS(0,0,0,0,0,0,0);
    # 13=IFCSIUNIT(*,.LENGTHUNIT.,.MILLI.,.METRE.);
    # 14=IFCSIUNIT(*,.AREAUNIT.,$,.SQUARE_METRE.);
    # 15=IFCSIUNIT(*,.VOLUMEUNIT.,$,.CUBIC_METRE.);
    # 16=IFCSIUNIT(*,.PLANEANGLEUNIT.,$,.RADIAN.);
    # 17=IFCMEASUREWITHUNIT(IFCPLANEANGLEMEASURE(0.017453292519943295),#16);
    # 18=IFCCONVERSIONBASEDUNIT(#12,.PLANEANGLEUNIT.,'DEGREE',#17);
    # 19=IFCUNITASSIGNMENT((#13,#14,#15,#18));
    # 20=IFCPROJECT('%(project_globalid)s',#5,'%(project_name)s',$,$,$,$,(#11),#19);
    ENDSEC;
    END-ISO-10303-21;
    """ % locals()

    # Write the template to a temporary file
    _, temp_filename = tempfile.mkstemp(suffix=".ifc")
    with open(temp_filename, "wb") as f:
        f.write(template.encode(encoding='utf-8'))

    return temp_filename


class Point:
    def __init__(self, *pt):
        self.point = pt

    def __getitem__(self, i):
        return self.point[i]

    def __add__(self, pt):
        return Point(self[0] + pt[0], self[1] + pt[1], self[2] + pt[2])

    def __sub__(self, pt):
        return Point(self[0] - pt[0], self[1] - pt[1], self[2] - pt[2])

    def __mul__(self, scalar):
        return Point(self[0] * scalar, self[1] * scalar, self[2] * scalar)

    def __truediv__(self, scalar):
        return Point(self[0] / scalar, self[1] / scalar, self[2] / scalar)

    def __neg__(self):
        return Point(-self[0], -self[1], -self[2])

    def __str__(self):
        return str(self.point)

    def __repr__(self):
        return repr(self.point)

    @property
    def lengthsq(self):
        return self[0] * self[0] + self[1] * self[1] + self[2] * self[2]

    @property
    def length(self):
        return math.sqrt(self.lengthsq)

    def normalize(self):
        return self / self.length

    def dot(self, pt):
        return self[0] * pt[0] + self[1] * pt[1] + self[2] * pt[2]

    def cross(self, pt):
        return Point(self[1] * pt[2] - self[2] * pt[1], self[2] * pt[0] - self[0] * pt[2],
                     self[0] * pt[1] - self[1] * pt[0])

    @property
    def tuple(self):
        return (self[0], self[1], self[2])

    @classmethod
    def origin(cls):
        return Point(0.0, 0.0, 0.0)

    @classmethod
    def x(cls):
        return Point(1.0, 0.0, 0.0)

    @classmethod
    def y(cls):
        return Point(0.0, 1.0, 0.0)

    @classmethod
    def z(cls):
        return Point(0.0, 0.0, 1.0)


class IFCBuilder:
    def __init__(self, filename, elevation=0.0, site_origin=Point(0.0, 0.0, 0.0), scan_point=Point(0.0, 0.0, 0.0)):
        self.filename = filename
        temp_filename = create_template(os.path.basename(filename))
        self.ifcfile = ifcopenshell.open(temp_filename)
        self.owner_history = self.ifcfile.by_type("IfcOwnerHistory")[0]
        self.project = self.ifcfile.by_type("IfcProject")[0]
        self.context = self.ifcfile.by_type("IfcGeometricRepresentationContext")[0]

        # IFC hierarchy creation
        self.site_placement = create_ifclocalplacement(self.ifcfile, point=site_origin.tuple)
        self.site = self.ifcfile.createIfcSite(create_guid(), self.owner_history, "Site", None,
                                               None, self.site_placement, None, None, "ELEMENT", None, None, None, None,
                                               None)

        self.building_placement = create_ifclocalplacement(
            self.ifcfile, relative_to=self.site_placement)
        self.building = self.ifcfile.createIfcBuilding(create_guid(
        ), self.owner_history, 'Building', None, None, self.building_placement, None, None, "ELEMENT", None, None, None)

        self.storey_placement = create_ifclocalplacement(
            self.ifcfile, relative_to=self.building_placement)
        self.elevation = elevation
        self.building_storey = self.ifcfile.createIfcBuildingStorey(create_guid(
        ), self.owner_history, 'Storey', None, None, self.storey_placement, None, None, "ELEMENT", elevation)

        self.container_storey = self.ifcfile.createIfcRelAggregates(create_guid(
        ), self.owner_history, "Building Container", None, self.building, [self.building_storey])
        self.container_site = self.ifcfile.createIfcRelAggregates(
            create_guid(), self.owner_history, "Site Container", None, self.site, [self.building])
        self.container_project = self.ifcfile.createIfcRelAggregates(
            create_guid(), self.owner_history, "Project Container", None, self.project, [self.site])

        self.scan_point = scan_point
        self.material_dict = {}

    def get_material(self, material_name, thickness):
        material_name_with_thickness = '%s_%s' % (material_name, thickness)
        if not material_name_with_thickness in self.material_dict:
            # Define and associate the wall material
            material = self.ifcfile.createIfcMaterial(material_name)
            material_layer = self.ifcfile.createIfcMaterialLayer(material, thickness, None)
            material_layer_set = self.ifcfile.createIfcMaterialLayerSet([material_layer], None)
            material_layer_set_usage = self.ifcfile.createIfcMaterialLayerSetUsage(
                material_layer_set, "AXIS2", "POSITIVE", 0.0)
            self.material_dict[material_name_with_thickness] = (
                material, material_layer, material_layer_set, material_layer_set_usage)
        return self.material_dict[material_name_with_thickness]

    def create_body_representation(self, start, end, height, thickness):
        x_axis = (end - start).normalize()
        z_axis = Point(0.0, 0.0, 1.0)

        extrusion_placement = create_ifcaxis2placement(self.ifcfile)

        point_list_extrusion_area = [
            start.tuple, (start + thickness).tuple,
            (end + thickness).tuple, end.tuple, start.tuple]
        solid = create_ifcextrudedareasolid(
            self.ifcfile, point_list_extrusion_area, extrusion_placement, z_axis.tuple, height)
        body_representation = self.ifcfile.createIfcShapeRepresentation(
            self.context, "Body", "SweptSolid", [solid])
        return body_representation

    def create_wall(self, raw_id, start, end, height, thickness, outter_dir):
        # Wall creation: Define the wall shape as a polyline axis and an extruded area solid
        wall_placement = create_ifclocalplacement(
            self.ifcfile, relative_to=self.storey_placement)

        # wall_dir = (end - start).normalize()
        # half_thickness = thickness / 2
        # polyline = create_ifcpolyline(self.ifcfile, [(start + outter_dir * half_thickness).tuple, (end + outter_dir * half_thickness).tuple])
        polyline = create_ifcpolyline(self.ifcfile, [start.tuple, end.tuple])
        axis_representation = self.ifcfile.createIfcShapeRepresentation(
            self.context, "Axis", "Curve2D", [polyline])

        # body_representation = self.create_body_representation(start - wall_dir * thickness, end + wall_dir * thickness, height, outter_dir * thickness)
        body_representation = self.create_body_representation(start, end, height, outter_dir * thickness)
        product_shape = self.ifcfile.createIfcProductDefinitionShape(
            None, None, [axis_representation, body_representation])

        wall = self.ifcfile.createIfcWallStandardCase(create_guid(
        ), self.owner_history, "Wall %s" % raw_id, "An awesome wall", None, wall_placement, product_shape, None)

        _, material_layer, _, material_layer_set_usage = self.get_material('wall material', thickness)
        self.ifcfile.createIfcRelAssociatesMaterial(create_guid(), self.owner_history, RelatedObjects=[
            wall], RelatingMaterial=material_layer_set_usage)

        quantity_values = [
            self.ifcfile.createIfcQuantityLength("Length", "Length of the wall", None, (end - start).length),
            self.ifcfile.createIfcQuantityLength("Height", "Height of the wall", None, height),
            self.ifcfile.createIfcQuantityLength("Thickness", "Thickness of the wall", None, thickness),
            self.ifcfile.createIfcQuantityArea(
                "Area", "Area of the front face", None, 5.0 * height),
            self.ifcfile.createIfcQuantityVolume(
                "Volume", "Volume of the wall", None, 5.0 * height * thickness)
        ]
        element_quantity = self.ifcfile.createIfcElementQuantity(
            create_guid(), self.owner_history, "BaseQuantities", None, None, quantity_values)
        self.ifcfile.createIfcRelDefinesByProperties(
            create_guid(), self.owner_history, None, None, [wall], element_quantity)

        self.ifcfile.createIfcRelContainedInSpatialStructure(create_guid(
        ), self.owner_history, "Building Storey Container", None, [wall, self.building_storey], self.building)
        return wall

    def create_openning(self, raw_id, start, end, height, thickness, owner_wall, wall_dir, outter_dir):
        # Create and associate an opening for the window in the wall
        openning_placement = create_ifclocalplacement(
            self.ifcfile, point=start.tuple, dir2=wall_dir.tuple, relative_to=owner_wall.ObjectPlacement)

        outter_dir = Point.y() * sign(Point.z().cross(wall_dir).dot(outter_dir))
        openning_representation = self.create_body_representation(Point.origin(),
                                                                  Point.x() * (end - start).dot(wall_dir), height,
                                                                  outter_dir * thickness)
        openning_shape = self.ifcfile.createIfcProductDefinitionShape(
            None, None, [openning_representation])
        openning_element = self.ifcfile.createIfcOpeningElement(create_guid(
        ), self.owner_history, "Opening %s" % raw_id, "Created By UNRE Scan To BIM", None, openning_placement,
            openning_shape, None)
        self.ifcfile.createIfcRelVoidsElement(
            create_guid(), self.owner_history, None, None, owner_wall, openning_element)

        return openning_element

    def create_window(self, raw_id, start, end, height, thickness, owner_wall, wall_dir, outter_dir):
        openning = self.create_openning(str(uuid.uuid4()), start, end, height, thickness, owner_wall, wall_dir,
                                        outter_dir)
        # Create a simplified representation for the Window
        window_placement = create_ifclocalplacement(
            self.ifcfile, relative_to=openning.ObjectPlacement)

        outter_dir = Point.y() * sign(Point.z().cross(wall_dir).dot(outter_dir))
        window_representation = self.create_body_representation(Point.origin(), Point.x() * (end - start).dot(wall_dir),
                                                                height, outter_dir * thickness)
        window_shape = self.ifcfile.createIfcProductDefinitionShape(
            None, None, [window_representation])
        window = self.ifcfile.createIfcWindow(create_guid(), self.owner_history, "Window %s" % raw_id,
                                              "Created By UNRE Scan To BIM", None, window_placement, window_shape,
                                              OverallHeight=height, OverallWidth=(end - start).length)

        # Relate the window to the opening element
        self.ifcfile.createIfcRelFillsElement(
            create_guid(), self.owner_history, None, None, openning, window)

        # Relate the window and wall to the building storey
        self.ifcfile.createIfcRelContainedInSpatialStructure(create_guid(
        ), self.owner_history, "Building Storey Container", None, [owner_wall, window], self.building_storey)

        return window

    def create_door(self, raw_id, start, end, height, thickness, owner_wall, wall_dir, outter_dir):
        openning = self.create_openning(str(uuid.uuid4()), start, end, height, thickness, owner_wall, wall_dir,
                                        outter_dir)
        # Create a simplified representation for the Window
        door_placement = create_ifclocalplacement(
            self.ifcfile, relative_to=openning.ObjectPlacement)

        outter_dir = Point.y() * sign(Point.z().cross(wall_dir).dot(outter_dir))
        door_representation = self.create_body_representation(Point.origin(), Point.x() * (end - start).dot(wall_dir),
                                                              height, outter_dir * thickness)
        door_shape = self.ifcfile.createIfcProductDefinitionShape(
            None, None, [door_representation])

        door = self.ifcfile.createIfcDoor(create_guid(), self.owner_history, "Door %s" % raw_id,
                                          "Created By UNRE Scan To BIM", None, door_placement, door_shape,
                                          OverallHeight=height, OverallWidth=(end - start).length)

        # Relate the door to the opening element
        self.ifcfile.createIfcRelFillsElement(
            create_guid(), self.owner_history, None, None, openning, door)

        # Relate the window and wall to the building storey
        self.ifcfile.createIfcRelContainedInSpatialStructure(create_guid(
        ), self.owner_history, "Building Storey Container", None, [owner_wall, door], self.building_storey)

        return door

    def write(self):
        # Write the contents of the file to disk
        self.ifcfile.write(self.filename)


def sign(x):
    if x > 0.0:
        return 1
    elif x < 0.0:
        return -1
    else:
        return 0


# if __name__ == '__main__':
#     parser = argparse.ArgumentParser(
#         'Automatically build IFC model from a floorplan generated by a 3D scanning of a room')
#     parser.add_argument('-i', '--inputFile')
#     parser.add_argument('-o', '--outputFile', default='')
#     parser.add_argument('-H', '--height', default=3000)
#     parser.add_argument('-t', '--thickness', default=200)
#     parser.add_argument('-u', '--unit', default='m')
#     args = parser.parse_args()
#     inputFile = args.inputFile
#     filename = os.path.abspath(args.file)
#     height = float(args.height) if isinstance(args.height, str) else args.height
#     thickness = float(args.thickness) if isinstance(args.thickness, str) else args.thickness
#     import uuid, os
#     # homepath = os.environ['HOMEPATH']
#     # filename = homepath + '\\Desktop\\roomcontour.xml'
#     import xmltodict
#
#     with open(inputFile, 'rb') as f:
#         xml = f.read()
#     xml_dict = xmltodict.parse(xml, encoding='utf8')
#     wall_info = xml_dict['root']['WallInfo']['LineData']
#     floor_info = xml_dict['root']['FloorInfo']['FloorShape']['LineData']
#     door_info = xml_dict['root']['DoorInfo']['DoorData']
#     win_info = xml_dict['root']['WinInfo']['WinData']
#
#     # height = 3
#     # thickness = 0.2
#     # builder = IFCBuilder('room.ifc')
#
#     # height = 3000
#     # thickness = 200
#     # builder = IFCBuilder(os.path.splitext(filename)[0] + '.ifc')
#     print("filename:", filename)
#     builder = IFCBuilder(filename)
#
#     wall_dict = {}
#     for wall in wall_info:
#         start_point = Point(float(wall['@StartX']), float(wall['@StartY']), float(wall['@StartZ']))
#         end_point = Point(float(wall['@EndX']), float(wall['@EndY']), float(wall['@EndZ']))
#         wall_dir = (end_point - start_point).normalize()
#         outter_dir = Point.z().cross(wall_dir)
#         outter_dir = outter_dir * sign((start_point - builder.scan_point).dot(outter_dir))
#         outter_dir = outter_dir.normalize()
#         # using the start point of the first wall as the origin of site
#         wall_obj = builder.create_wall(wall['@WallId'], start_point, end_point, height, thickness, outter_dir)
#         wall_dict[wall['@WallId']] = (wall_obj, wall_dir, outter_dir)
#     for win in win_info:
#         start_point = Point(float(win['@StartX']), float(win['@StartY']), float(win['@StartZ']))
#         end_point = Point(float(win['@EndX']), float(win['@EndY']), float(win['@EndZ']))
#         builder.create_window(str(uuid.uuid4()), start_point, end_point, float(win['@Height']), 300,
#                               *wall_dict[win['@WallId']])
#     for door in door_info:
#         # start_point = Point(float(door['@StartX']) / 1000, float(door['@StartY']) / 1000, float(door['@StartZ']) / 1000)
#         # end_point = Point(float(door['@EndX']) / 1000, float(door['@EndY']) / 1000, float(door['@EndZ']) / 1000)
#         # door_openning = builder.create_openning(str(uuid.uuid4()),
#         #                                         start_point,
#         #                                         end_point, float(door['@Height']) / 1000,
#         #                                         0.3,
#         #                                         wall_dict[door['@WallId']])
#         # builder.create_window(str(uuid.uuid4()), start_point, end_point, float(door['@Height']) / 1000, 0.3,
#         #                     door_openning)
#
#         start_point = Point(float(door['@StartX']), float(door['@StartY']), float(door['@StartZ']))
#         end_point = Point(float(door['@EndX']), float(door['@EndY']), float(door['@EndZ']))
#         builder.create_door(str(uuid.uuid4()), start_point, end_point, float(door['@Height']), 300,
#                             *wall_dict[door['@WallId']])
#
#     builder.write()
#     print("end")
